﻿#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//struct S
//{
//	char c1;//1
//	int i;//4
//	char c2;//1
//};
//int main()
//{
//	struct S s = { 0 };
//	printf("%zd\n", sizeof(s));//12 
//	//sizeof的返回值是size_t类型的 打印size_t类型的需要用%zd来打印
//	return 0;
//}
//结构体内存对齐
//练习题目
//struct S2
//{
//	char c1;
//	char c2;
//	int i;
//};

//struct S3
//{
//	double d;//自身大小是8 vs中默认对齐数为8
//	char c;//自身大小为1  默认对齐数为8
//	int i;//自身大小为4 默认对齐数为8  //所有对齐数最大的是8 结构体总大小是16 是8的倍数 所以s3为16个字节 但是浪费了3个字节
//};
//
//int main()
//{
//	//struct S2 s2 = { 0 };
//	//printf("%zd\n", sizeof(s2));//size_t 用%zd来打印 结果是8 用结构体对齐规则来计算
//
//	struct S3 s3 = { 0 };
//	printf("%zd\n", sizeof(s3));//16
//	return 0;
//}

//struct S3
//{
//	double d;
//	char c;
//	int i;
//};
//struct S4
//{
//	char c1;//自身对齐数 1  默认对齐数8
//	struct S3 s3;//结构体里边嵌套了一个结构体
//	//如果嵌套了结构体的情况，嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处，结构
//	//体的整体⼤⼩就是所有最⼤对⻬数（含嵌套结构体中成员的对⻬数）的整数倍。
//	//嵌套结构体中的最大对齐数也是8
//	double d;//自身对齐数是8 默认对齐数是8
//};
//int main()
//{
//	struct S4 s4 = { 0 };
//	printf("%zd\n", sizeof(s4));//32
//	return 0;
//}


//为什么会存在内存对齐呢？
//1.平台原因
//2.性能原因 在不对齐的话存在时间上的浪费 可能需要多次访问才能获取想要的数值
//总的来说 结构体的内存对齐就是拿空间来换取时间

//为了不那么浪费空间 我们在设计结构体的时候，既要满足对齐，又要节省空间
//应该做的是让占用空间小的成员尽量集中在一起

//修改默认对齐数
//#pragma这个预处理指令，可以修改编译器的默认对齐数
//struct S
//{
//	char c1;
//	int i;
//	char c2;
//};
//int main()
//{
//	printf("%zd\n", sizeof(struct S));//12 这是采用默认对齐数的结果
//	return 0;
//}
//
//如果要修改默认对齐数的结果是什么呢？一般在你觉得编译器的默认对齐数不好的情况下 就要修改 改的话也不要乱改 一般都是2的次方数
//#pragma pack(1)//设置默认对齐数为1  
//struct S
//{
//	char c1;// 默认对齐数1 自身是一个字节 结果取1个字节
//	int i;//默认对齐数1 自身是四个字节 结果取1个字节
//	char c2;//默认对齐数是1 自身是一个字节 结果取1
//};
//#pragma pack()//取消默认对齐数
//int main()
//{
//	printf("%zd\n", sizeof(struct S));
//	return 0;
//}

////结构体传参
//struct S
//{
//	int arr[1000];
//	int n;
//	double d;
//};
//void print1(struct S tmp)//参数部分使用结构体变量
//{
//	int i = 0;
//	for (i = 0; i < 5; i++)
//	{
//		printf("%d ", tmp.arr[i]);
//	}//这是打印数组元素
//	printf("%d ", tmp.n);
//	printf("%lf\n", tmp.d);
//}         //这个const是防止ps在返回到s被修改了
//void print2(const struct S* ps)//因为传过来的是地址 所以拿指针变量来接收 在这创建的是指针变量 占用的内存仅仅是四个或者八个字节(看平台)
//{              //这样传参的时候 消耗的就会低很多了
//	int i = 0;
//	for (i = 0; i < 5; i++)
//	{
//		printf("%d ", ps->arr[i]);//ps指向的是s s有成员数组arr、n和d
//		//这样就打印了数组的前五个元素了
//	}
//	printf("%d ", ps->n);
//	printf("%lf\n", ps->d);//指针ps拿到的是s的地址 是可以直接找回s中的数据
//}
//int main()
//{
//	struct S s = { {1,2,3,4,5},100,3.14 };
//	print1(s);//把s里的数据拷贝一份给tmp 打印tmp的值和打印s的值是一模一样的
//
//	print2(&s);//这次传参传的仅仅是个地址
//	//函数传参的时候，参数是需要压栈，会有时间和空间上的系统开销
//	//如果传递一个结构体对象的时候，结构体过大，参数压栈的系统开销比较大，所以会导致性能的下降
//	return 0;
//}//所以在进行结构体传参的时候，尽量要选择传地址	

//结构体实现位段
//位段的声明和结构是类似的，有两个不同
//1.位段的成员必须是int  unsigned int  或signed int，在c99中位段成员的类型也可以选择其他类型
//2.位段的成员名后边有一个冒号和一个数字
//struct S //位段式的结构  这个位指的是二进制的位
//{
//	int _a:2; //本身的话int类型是4个字节 32个bit位 但是在赋值的时候一般都是只占用几个bit位 会造成浪费
//	//这样写的话就明确占了几个bit位 不会造成浪费  只占用2个
//	int _b:5;//只占用5个bit位
//	int _c:10;//只占用10个bit位
//	int _d:30;//只占用30个bit位
//	//这样位段式的结构在一定程度上就减少了内存的浪费 节省空间
//	//位段是专门用来节省内存的
//};
//int main()
//{
//	printf("%zd\n", sizeof(struct S));//结果是8
//	return 0;
//}

//位段的内存分配
//1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
//2. 位段的空间上是按照需要以4个字节（ int ）或者1个字节（ char ）的⽅式来开辟的。
//3. 位段涉及很多不确定因素，位段是不跨平台的，注重可移植的程序应该避免使⽤位段。
//struct S
//{
//	char a : 3;
//	char b : 4;
//	char c : 5;
//	char d : 4;
//};
//int main()
//{
//	//申请了内存空间并初始化为0 
//	struct S s = { 0 };//在内存中为s开辟一块空间 虽说实际上编译器一次性会把内存分配好 但是现在拆开说明会更好理解
//	//首先在内存上开辟1字节(8bit)的空间  这个a要用3bit位 但是是从左向右还是从右向左使用呢？遗憾的是，c语言标准并没有规定
//	//那么这个时候就取决于编译器了 在vs平台是从右向左使用 
//	//这个时候继续分配空间给b 4bit位 还剩下1bit位
//	//剩余的空间，不足下一个成员使用的时候，是浪费呢？还是继续使用呢？ 在vs上是浪费了
//	//还需要为剩余的成员变量做内存分配 这时候内存又开辟了1字节的空间 c占5个bit位(从右向左使用的) 剩余的3bit位不足以让d使用
//	//抛弃剩余的空间 继续申请开辟1字节的空间 还是从右向左使用
//	//d占了4bit 剩余的4bit位也浪费了
//	//从头到尾算下来一共申请了3字节的空间
//	s.a = 10;//10的二进制为00001010 8个bit位 但是在分配的时候 a就有3bit位 这时候发生截断 存的就是010 其余的丢了 存不下
//	s.b = 12;//12的二进制为00001100 也是8个bit位 b有4个bit位 能把1100存进去 
//	s.c = 3;//3的二进制序列00000011 c有5bit位 能存的就是00011
//	s.d = 4;//4的二进制序列为00000100 d有4bit位 能存进去0100
//	printf("%zd\n", sizeof(s));//3
//	
//	//把s中存的数据以十六进制进行观察 四个二进制位为1个16进制
//	//0110 0010 0000 0011 0000 0100
//	//6      2    0    3    0   4
//	//内存放的就是0x620304
//	//可以调试-内存 把&s调出来看一下
//
//	//整型的话是先开辟1个字节的空间 然后分配到各个成员变量中
//	return 0;
//}

//位段的跨平台问题
//1. int 位段被当成有符号数还是⽆符号数是不确定的。
//2. 位段中最⼤位的数⽬不能确定。（16位机器最⼤16，32位机器最⼤32，写成27，在16位机器会出问题。
// 32位和64位的机器上int的长度是4个字节  16位的机器上int的长度是2个字节的
//3. 位段中的成员在内存中从左向右分配，还是从右向左分配标准尚未定义。
//4. 当⼀个结构包含两个位段，第⼆个位段成员⽐较⼤，⽆法容纳于第⼀个位段剩余的位时，是舍弃剩余的位还是利⽤，这是不确定的。
//怎么解决这个问题呢？
//根据不同的平台写不同的代码

//位段使用的注意事项
//位段的⼏个成员共同用⼀个字节，这样有些成员的起始位置并不是某个字节的起始位置，那么这些位置处是没有地址的。
// 内存中每个字节分配⼀个地址，⼀个字节内部的bit位是没有地址的。
//所以不能对位段的成员使⽤&操作符，因为有的根本就没有地址。这样就不能使⽤scanf直接给位段的成员输⼊值，
// 只能是先输⼊放在⼀个变量中，然后赋值给位段的成员。
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;

};
int main()
{
	struct A sa = { 0 };
	//scanf("%d", &(sa._b));//error 
	//不能对位段式的结构体成员取地址

	//正确的示范
	int b = 0;
	scanf("%d", &b);
	//先把b的值输入 
	sa._b = b;
	//然后把b的值在赋值到位段成员_b
	return 0;
}
//使用位段结构里的时候 尽量让结构体里边的成员变量都是相同的数据类型 否则内存开辟的时候又跟期望的不一样了
//只有在结构体中才能使用位段